Path,
/// represents the central registry
Registry,
+ /// represents a local filesystem-based registry
+ LocalRegistry,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
SourceIdInner { kind: Kind::Registry, ref url, .. } => {
format!("registry+{}", url)
}
+ SourceIdInner { kind: Kind::LocalRegistry, ref url, .. } => {
+ format!("local-registry+{}", url)
+ }
}
}
SourceId::new(Kind::Registry, url.clone())
}
+ pub fn for_local_registry(path: &Path) -> CargoResult<SourceId> {
+ let url = try!(path.to_url());
+ Ok(SourceId::new(Kind::LocalRegistry, url))
+ }
+
/// Returns the `SourceId` corresponding to the main repository.
///
/// This is the main cargo registry by default, but it can be overridden in
self.inner.kind == Kind::Path
}
pub fn is_registry(&self) -> bool {
- self.inner.kind == Kind::Registry
+ self.inner.kind == Kind::Registry || self.inner.kind == Kind::LocalRegistry
}
pub fn is_git(&self) -> bool {
Box::new(PathSource::new(&path, self, config))
}
Kind::Registry => Box::new(RegistrySource::remote(self, config)),
+ Kind::LocalRegistry => {
+ let path = match self.inner.url.to_file_path() {
+ Ok(p) => p,
+ Err(()) => panic!("path sources cannot be remote"),
+ };
+ Box::new(RegistrySource::local(self, &path, config))
+ }
}
}
}
Ok(())
}
- SourceIdInner { kind: Kind::Registry, ref url, .. } => {
+ SourceIdInner { kind: Kind::Registry, ref url, .. } |
+ SourceIdInner { kind: Kind::LocalRegistry, ref url, .. } => {
write!(f, "registry {}", url)
}
}
let url = try!(url(val, &format!("source.{}.registry", name)));
srcs.push(SourceId::for_registry(&url));
}
+ if let Some(val) = table.get("local-registry") {
+ let (s, path) = try!(val.string(&format!("source.{}.local-registry",
+ name)));
+ let mut path = path.to_path_buf();
+ path.pop();
+ path.pop();
+ path.push(s);
+ srcs.push(try!(SourceId::for_local_registry(&path)));
+ }
let mut srcs = srcs.into_iter();
let src = try!(srcs.next().chain_error(|| {
- human(format!("no source URL specified for `source.{}`, needs \
- `registry` defined", name))
+ human(format!("no source URL specified for `source.{}`, need \
+ either `registry` or `local-registry` defined",
+ name))
}));
if srcs.next().is_some() {
return Err(human(format!("more than one source URL specified for \
use std::collections::HashMap;
use std::io::prelude::*;
-use std::fs::File;
+use std::fs::{File, OpenOptions};
use std::path::Path;
use rustc_serialize::json;
if self.cache.contains_key(name) {
return Ok(self.cache.get(name).unwrap());
}
+ // If the lock file doesn't already exist then this'll cause *someone*
+ // to create it. We don't actually care who creates it, and if it's
+ // already there this should have no effect.
+ drop(OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open(self.path.join(INDEX_LOCK).into_path_unlocked()));
let lock = self.path.open_ro(Path::new(INDEX_LOCK),
self.config,
"the registry index");
--- /dev/null
+use std::io::SeekFrom;
+use std::io::prelude::*;
+use std::path::Path;
+
+use rustc_serialize::hex::ToHex;
+
+use core::PackageId;
+use sources::registry::{RegistryData, RegistryConfig};
+use util::{Config, CargoResult, ChainError, human, Sha256, Filesystem};
+use util::FileLock;
+
+pub struct LocalRegistry<'cfg> {
+ index_path: Filesystem,
+ root: Filesystem,
+ src_path: Filesystem,
+ config: &'cfg Config,
+}
+
+impl<'cfg> LocalRegistry<'cfg> {
+ pub fn new(root: &Path,
+ config: &'cfg Config,
+ name: &str) -> LocalRegistry<'cfg> {
+ LocalRegistry {
+ src_path: config.registry_source_path().join(name),
+ index_path: Filesystem::new(root.join("index")),
+ root: Filesystem::new(root.to_path_buf()),
+ config: config,
+ }
+ }
+}
+
+impl<'cfg> RegistryData for LocalRegistry<'cfg> {
+ fn index_path(&self) -> &Filesystem {
+ &self.index_path
+ }
+
+ fn config(&self) -> CargoResult<Option<RegistryConfig>> {
+ // Local registries don't have configuration for remote APIs or anything
+ // like that
+ Ok(None)
+ }
+
+ fn update_index(&mut self) -> CargoResult<()> {
+ // Nothing to update, we just use what's on disk. Verify it actually
+ // exists though. We don't use any locks as we're just checking whether
+ // these directories exist.
+ let root = self.root.clone().into_path_unlocked();
+ if !root.is_dir() {
+ bail!("local registry path is not a directory: {}",
+ root.display())
+ }
+ let index_path = self.index_path.clone().into_path_unlocked();
+ if !index_path.is_dir() {
+ bail!("local registry index path is not a directory: {}",
+ index_path.display())
+ }
+ Ok(())
+ }
+
+ fn download(&mut self, pkg: &PackageId, checksum: &str)
+ -> CargoResult<FileLock> {
+ let crate_file = format!("{}-{}.crate", pkg.name(), pkg.version());
+ let mut crate_file = try!(self.root.open_ro(&crate_file,
+ self.config,
+ "crate file"));
+
+ // If we've already got an unpacked version of this crate, then skip the
+ // checksum below as it is in theory already verified.
+ let dst = format!("{}-{}", pkg.name(), pkg.version());
+ if self.src_path.join(dst).into_path_unlocked().exists() {
+ return Ok(crate_file)
+ }
+
+ try!(self.config.shell().status("Unpacking", pkg));
+
+ // We don't actually need to download anything per-se, we just need to
+ // verify the checksum matches the .crate file itself.
+ let mut data = Vec::new();
+ try!(crate_file.read_to_end(&mut data).chain_error(|| {
+ human(format!("failed to read `{}`", crate_file.path().display()))
+ }));
+ let mut state = Sha256::new();
+ state.update(&data);
+ if state.finish().to_hex() != checksum {
+ bail!("failed to verify the checksum of `{}`", pkg)
+ }
+
+ try!(crate_file.seek(SeekFrom::Start(0)));
+
+ Ok(crate_file)
+ }
+}
use std::collections::HashMap;
use std::fs::File;
-use std::path::PathBuf;
+use std::path::{PathBuf, Path};
use flate2::read::GzDecoder;
use tar::Archive;
mod index;
mod remote;
+mod local;
fn short_name(id: &SourceId) -> String {
let hash = hex::short_hash(id);
RegistrySource::new(source_id, config, &name, Box::new(ops))
}
+ pub fn local(source_id: &SourceId,
+ path: &Path,
+ config: &'cfg Config) -> RegistrySource<'cfg> {
+ let name = short_name(source_id);
+ let ops = local::LocalRegistry::new(path, config, &name);
+ RegistrySource::new(source_id, config, &name, Box::new(ops))
+ }
+
fn new(source_id: &SourceId,
config: &'cfg Config,
name: &str,
}
fn update_index(&mut self) -> CargoResult<()> {
+ // Ensure that this'll actually succeed later on.
+ try!(ops::http_handle(self.config));
+
+ // Then we actually update the index
try!(self.index_path.create_dir());
let lock = try!(self.index_path.open_rw(Path::new(INDEX_LOCK),
self.config,
assert_that(foo.cargo_process("publish").arg("-v"),
execs().with_status(101).with_stderr("\
-[UPDATING] registry `[..]`
-[ERROR] invalid configuration for key `http.proxy`
+error: failed to update registry [..]
+
+Caused by:
+ invalid configuration for key `http.proxy`
expected a string, but found a boolean for `http.proxy` in [..]config
"));
}
assert_that(p.cargo_process("build"),
execs().with_status(101).with_stderr("\
-error: no source URL specified for `source.foo`, needs [..]
+error: no source URL specified for `source.foo`, need [..]
"));
}
}
#[test]
-#[ignore]
fn bad_source_config7() {
let p = project("foo")
.file("Cargo.toml", r#"
.file(".cargo/config", r#"
[source.foo]
registry = 'http://example.com'
- directory = 'file:///another/file'
+ local-registry = 'file:///another/file'
"#);
Package::new("bar", "0.1.0").publish();
error: more than one source URL specified for `source.foo`
"));
}
-
-#[test]
-#[ignore]
-fn bad_source_config8() {
- let p = project("foo")
- .file("Cargo.toml", r#"
- [package]
- name = "foo"
- version = "0.0.0"
- authors = []
-
- [dependencies]
- bar = "*"
- "#)
- .file("src/lib.rs", "")
- .file(".cargo/config", r#"
- [source.foo]
- directory = 'file://another/file'
- "#);
-
- assert_that(p.cargo_process("build"),
- execs().with_status(101).with_stderr("\
-error: failed to convert `file://another/file` to an absolute path (configured in [..])
-"));
-}
("[VERIFYING]", " Verifying"),
("[ARCHIVING]", " Archiving"),
("[INSTALLING]", " Installing"),
- ("[REPLACING]", " Replacing")
+ ("[REPLACING]", " Replacing"),
+ ("[UNPACKING]", " Unpacking"),
];
let mut result = input.to_owned();
for &(pat, subst) in macros.iter() {
files: Vec<(String, String)>,
yanked: bool,
features: HashMap<String, Vec<String>>,
+ local: bool,
}
struct Dependency {
files: Vec::new(),
yanked: false,
features: HashMap::new(),
+ local: false,
}
}
+ pub fn local(&mut self, local: bool) -> &mut Package {
+ self.local = local;
+ self
+ }
+
pub fn file(&mut self, name: &str, contents: &str) -> &mut Package {
self.files.push((name.to_string(), contents.to_string()));
self
};
// Write file/line in the index
- let dst = registry_path().join(&file);
+ let dst = if self.local {
+ registry_path().join("index").join(&file)
+ } else {
+ registry_path().join(&file)
+ };
let mut prev = String::new();
let _ = File::open(&dst).and_then(|mut f| f.read_to_string(&mut prev));
t!(fs::create_dir_all(dst.parent().unwrap()));
.write_all((prev + &line[..] + "\n").as_bytes()));
// Add the new file to the index
- let repo = t!(git2::Repository::open(®istry_path()));
- let mut index = t!(repo.index());
- t!(index.add_path(Path::new(&file)));
- t!(index.write());
- let id = t!(index.write_tree());
-
- // Commit this change
- let tree = t!(repo.find_tree(id));
- let sig = t!(repo.signature());
- let parent = t!(repo.refname_to_id("refs/heads/master"));
- let parent = t!(repo.find_commit(parent));
- t!(repo.commit(Some("HEAD"), &sig, &sig,
- "Another commit", &tree,
- &[&parent]));
+ if !self.local {
+ let repo = t!(git2::Repository::open(®istry_path()));
+ let mut index = t!(repo.index());
+ t!(index.add_path(Path::new(&file)));
+ t!(index.write());
+ let id = t!(index.write_tree());
+
+ // Commit this change
+ let tree = t!(repo.find_tree(id));
+ let sig = t!(repo.signature());
+ let parent = t!(repo.refname_to_id("refs/heads/master"));
+ let parent = t!(repo.find_commit(parent));
+ t!(repo.commit(Some("HEAD"), &sig, &sig,
+ "Another commit", &tree,
+ &[&parent]));
+ }
}
fn make_archive(&self) {
}
pub fn archive_dst(&self) -> PathBuf {
- dl_path().join(&self.name).join(&self.vers).join("download")
+ if self.local {
+ registry_path().join(format!("{}-{}.crate", self.name,
+ self.vers))
+ } else {
+ dl_path().join(&self.name).join(&self.vers).join("download")
+ }
}
}
--- /dev/null
+#[macro_use]
+extern crate cargotest;
+extern crate hamcrest;
+
+use std::fs::{self, File};
+use std::io::prelude::*;
+
+use cargotest::support::paths::{self, CargoPathExt};
+use cargotest::support::registry::Package;
+use cargotest::support::{project, execs};
+use hamcrest::assert_that;
+
+fn setup() {
+ let root = paths::root();
+ t!(fs::create_dir(&root.join(".cargo")));
+ t!(t!(File::create(root.join(".cargo/config"))).write_all(br#"
+ [source.crates-io]
+ registry = 'https://wut'
+ replace-with = 'my-awesome-local-registry'
+
+ [source.my-awesome-local-registry]
+ local-registry = 'registry'
+ "#));
+}
+
+#[test]
+fn simple() {
+ setup();
+ Package::new("foo", "0.0.1")
+ .local(true)
+ .file("src/lib.rs", "pub fn foo() {}")
+ .publish();
+
+ let p = project("bar")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "bar"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "0.0.1"
+ "#)
+ .file("src/lib.rs", r#"
+ extern crate foo;
+ pub fn bar() {
+ foo::foo();
+ }
+ "#);
+
+ assert_that(p.cargo_process("build"),
+ execs().with_status(0).with_stderr(&format!("\
+[UNPACKING] foo v0.0.1 ([..])
+[COMPILING] foo v0.0.1
+[COMPILING] bar v0.0.1 ({dir})
+",
+ dir = p.url())));
+ assert_that(p.cargo("build"), execs().with_status(0).with_stderr(""));
+ assert_that(p.cargo("test"), execs().with_status(0));
+}
+
+#[test]
+fn multiple_versions() {
+ setup();
+ Package::new("foo", "0.0.1").local(true).publish();
+ Package::new("foo", "0.1.0")
+ .local(true)
+ .file("src/lib.rs", "pub fn foo() {}")
+ .publish();
+
+ let p = project("bar")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "bar"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "*"
+ "#)
+ .file("src/lib.rs", r#"
+ extern crate foo;
+ pub fn bar() {
+ foo::foo();
+ }
+ "#);
+
+ assert_that(p.cargo_process("build"),
+ execs().with_status(0).with_stderr(&format!("\
+[UNPACKING] foo v0.1.0 ([..])
+[COMPILING] foo v0.1.0
+[COMPILING] bar v0.0.1 ({dir})
+",
+ dir = p.url())));
+
+ Package::new("foo", "0.2.0")
+ .local(true)
+ .file("src/lib.rs", "pub fn foo() {}")
+ .publish();
+
+ assert_that(p.cargo("update").arg("-v"),
+ execs().with_status(0).with_stderr("\
+[UPDATING] foo v0.1.0 -> v0.2.0
+"));
+}
+
+#[test]
+fn multiple_names() {
+ setup();
+ Package::new("foo", "0.0.1")
+ .local(true)
+ .file("src/lib.rs", "pub fn foo() {}")
+ .publish();
+ Package::new("bar", "0.1.0")
+ .local(true)
+ .file("src/lib.rs", "pub fn bar() {}")
+ .publish();
+
+ let p = project("local")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "local"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "*"
+ bar = "*"
+ "#)
+ .file("src/lib.rs", r#"
+ extern crate foo;
+ extern crate bar;
+ pub fn local() {
+ foo::foo();
+ bar::bar();
+ }
+ "#);
+
+ assert_that(p.cargo_process("build"),
+ execs().with_status(0).with_stderr(&format!("\
+[UNPACKING] [..]
+[UNPACKING] [..]
+[COMPILING] [..]
+[COMPILING] [..]
+[COMPILING] local v0.0.1 ({dir})
+",
+ dir = p.url())));
+}
+
+#[test]
+fn interdependent() {
+ setup();
+ Package::new("foo", "0.0.1")
+ .local(true)
+ .file("src/lib.rs", "pub fn foo() {}")
+ .publish();
+ Package::new("bar", "0.1.0")
+ .local(true)
+ .dep("foo", "*")
+ .file("src/lib.rs", "extern crate foo; pub fn bar() {}")
+ .publish();
+
+ let p = project("local")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "local"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "*"
+ bar = "*"
+ "#)
+ .file("src/lib.rs", r#"
+ extern crate foo;
+ extern crate bar;
+ pub fn local() {
+ foo::foo();
+ bar::bar();
+ }
+ "#);
+
+ assert_that(p.cargo_process("build"),
+ execs().with_status(0).with_stderr(&format!("\
+[UNPACKING] [..]
+[UNPACKING] [..]
+[COMPILING] foo v0.0.1
+[COMPILING] bar v0.1.0
+[COMPILING] local v0.0.1 ({dir})
+",
+ dir = p.url())));
+}
+
+#[test]
+fn path_dep_rewritten() {
+ setup();
+ Package::new("foo", "0.0.1")
+ .local(true)
+ .file("src/lib.rs", "pub fn foo() {}")
+ .publish();
+ Package::new("bar", "0.1.0")
+ .local(true)
+ .dep("foo", "*")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "bar"
+ version = "0.1.0"
+ authors = []
+
+ [dependencies]
+ foo = { path = "foo", version = "*" }
+ "#)
+ .file("src/lib.rs", "extern crate foo; pub fn bar() {}")
+ .file("foo/Cargo.toml", r#"
+ [project]
+ name = "foo"
+ version = "0.0.1"
+ authors = []
+ "#)
+ .file("foo/src/lib.rs", "pub fn foo() {}")
+ .publish();
+
+ let p = project("local")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "local"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "*"
+ bar = "*"
+ "#)
+ .file("src/lib.rs", r#"
+ extern crate foo;
+ extern crate bar;
+ pub fn local() {
+ foo::foo();
+ bar::bar();
+ }
+ "#);
+
+ assert_that(p.cargo_process("build"),
+ execs().with_status(0).with_stderr(&format!("\
+[UNPACKING] [..]
+[UNPACKING] [..]
+[COMPILING] foo v0.0.1
+[COMPILING] bar v0.1.0
+[COMPILING] local v0.0.1 ({dir})
+",
+ dir = p.url())));
+}
+
+#[test]
+fn invalid_dir_bad() {
+ setup();
+ let p = project("local")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "local"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "*"
+ "#)
+ .file("src/lib.rs", "")
+ .file(".cargo/config", r#"
+ [source.crates-io]
+ registry = 'https://wut'
+ replace-with = 'my-awesome-local-directory'
+
+ [source.my-awesome-local-directory]
+ local-registry = '/path/to/nowhere'
+ "#);
+
+
+ assert_that(p.cargo_process("build"),
+ execs().with_status(101).with_stderr("\
+[ERROR] failed to load source for a dependency on `foo`
+
+Caused by:
+ Unable to update registry https://[..]
+
+Caused by:
+ failed to update replaced source `registry https://[..]`
+
+Caused by:
+ local registry path is not a directory: [..]path[..]to[..]nowhere
+"));
+}
+
+#[test]
+fn different_directory_replacing_the_registry_is_bad() {
+ setup();
+
+ // Move our test's .cargo/config to a temporary location and publish a
+ // registry package we're going to use first.
+ let config = paths::root().join(".cargo");
+ let config_tmp = paths::root().join(".cargo-old");
+ t!(fs::rename(&config, &config_tmp));
+
+ let p = project("local")
+ .file("Cargo.toml", r#"
+ [project]
+ name = "local"
+ version = "0.0.1"
+ authors = []
+
+ [dependencies]
+ foo = "*"
+ "#)
+ .file("src/lib.rs", "");
+ p.build();
+
+ // Generate a lock file against the crates.io registry
+ Package::new("foo", "0.0.1").publish();
+ assert_that(p.cargo("build"), execs().with_status(0));
+
+ // Switch back to our directory source, and now that we're replacing
+ // crates.io make sure that this fails because we're replacing with a
+ // different checksum
+ config.rm_rf();
+ t!(fs::rename(&config_tmp, &config));
+ Package::new("foo", "0.0.1")
+ .file("src/lib.rs", "invalid")
+ .local(true)
+ .publish();
+
+ assert_that(p.cargo("build"),
+ execs().with_status(101).with_stderr("\
+[ERROR] checksum for `foo v0.0.1` changed between lock files
+
+this could be indicative of a few possible errors:
+
+ * the lock file is corrupt
+ * a replacement source in use (e.g. a mirror) returned a different checksum
+ * the source itself may be corrupt in one way or another
+
+unable to verify that `foo v0.0.1` was the same as before in any situation
+
+"));
+}
let pkg = Package::new("bad-cksum", "0.0.1");
pkg.publish();
- File::create(&pkg.archive_dst()).unwrap();
+ t!(File::create(&pkg.archive_dst()));
assert_that(p.cargo_process("build").arg("-v"),
execs().with_status(101).with_stderr("\
assert_that(p.cargo("build"),
execs().with_status(0));
- fs::remove_dir_all(®istry::registry_path().join("3")).unwrap();
+ registry::registry_path().join("3").rm_rf();
Package::new("bar", "0.0.1").yanked(true).publish();
#[test]
fn login_with_no_cargo_dir() {
let home = paths::home().join("new-home");
- fs::create_dir(&home).unwrap();
+ t!(fs::create_dir(&home));
assert_that(cargo_process().arg("login").arg("foo").arg("-v"),
execs().with_status(0));
}
",
dir = p.url())));
- t!(File::create(&p.root().join("a/Cargo.toml"))).write_all(br#"
+ t!(t!(File::create(&p.root().join("a/Cargo.toml"))).write_all(br#"
[project]
name = "a"
version = "0.0.1"
[dependencies]
bar = "0.1.0"
- "#).unwrap();
+ "#));
Package::new("bar", "0.1.0").publish();
println!("second");
Package::new("a", "0.1.1").publish();
let registry = paths::home().join(".cargo/registry");
let backup = paths::root().join("registry-backup");
- fs::rename(®istry, &backup).unwrap();
+ t!(fs::rename(®istry, &backup));
// Generate a Cargo.lock with the newer version, and then move the old copy
// of the registry back into place.
.file("src/main.rs", "fn main() {}");
assert_that(p2.cargo_process("build"),
execs().with_status(0));
- fs::remove_dir_all(®istry).unwrap();
- fs::rename(&backup, ®istry).unwrap();
- fs::rename(p2.root().join("Cargo.lock"), p.root().join("Cargo.lock")).unwrap();
+ registry.rm_rf();
+ t!(fs::rename(&backup, ®istry));
+ t!(fs::rename(p2.root().join("Cargo.lock"), p.root().join("Cargo.lock")));
// Finally, build the first project again (with our newer Cargo.lock) which
// should force an update of the old registry, download the new crate, and
assert_that(p.cargo("build").arg("--frozen"),
execs().with_status(101).with_stderr("\
-[UPDATING] registry `[..]`
error: failed to load source for a dependency on `foo`
Caused by:
Unable to update registry [..]
Caused by:
- failed to fetch `[..]`
-
-Caused by:
- attempting to update a git repository, but --frozen was specified
+ attempting to make an HTTP request, but --frozen was specified
"));
}